package com.fsh.subject.infra.config;

import com.fsh.subject.common.util.LoginUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.*;

/**
 * 作者：fsh
 * 日期：2024/01/15
 * <p>
 * 描述：MyBatis 拦截器.
 * 这段代码主要实现了一个拦截器方法，用于在执行某些数据库操作之前进行一些处理
 */
@Component
@Slf4j
@Intercepts({@Signature(type = Executor.class, method = "update", args = {
        MappedStatement.class, Object.class
})})
public class MybatisInterceptor implements Interceptor {

    /**
     * 覆盖父类方法，拦截方法执行。当该方法被调用时，会先执行此方法，然后再执行原来的方法。
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取传入的MappedStatement对象，它代表一个映射语句。
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        // 获取该语句的SQL命令类型。
        SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
        // 获取传入的参数。
        Object parameter = invocation.getArgs()[1];
        // 获取当前登录用户的id。
        String loginId = LoginUtil.getLoginId("loginId");
        // 如果参数为空或者登录id为空，则直接执行原来的方法。
        if (parameter == null||StringUtils.isBlank(loginId)) {
            return invocation.proceed();
        }
        // 如果SQL命令类型是INSERT或UPDATE，则替换参数中的某些属性。
        if (SqlCommandType.INSERT == sqlCommandType || SqlCommandType.UPDATE == sqlCommandType) {
            replaceEntityProperty(parameter, loginId, sqlCommandType);
        }
        // 执行原来的方法。
        return invocation.proceed();
    }

    /**
     * 提取的私有方法：
     *      替换实体类的属性值。
     */
    private void replaceEntityProperty(Object parameter, String loginId, SqlCommandType sqlCommandType) {
        // 如果参数是一个Map，则对Map中的每个值进行替换。
        if (parameter instanceof Map) {
            for (Object value : ((Map<?, ?>) parameter).values()) {
                replace(value,loginId,sqlCommandType);
            }
        } else { // 否则直接替换参数的值。
            replace(parameter, loginId, sqlCommandType);
        }
    }

    /**
     * 提取的私有方法：
     *      根据SQL命令类型处理参数的替换。
     */
    private void replace(Object parameter, String loginId, SqlCommandType sqlCommandType) {
        if (SqlCommandType.INSERT == sqlCommandType) { // 如果命令类型是INSERT，则处理插入操作。
            dealInsert(parameter, loginId);
        } else { // 否则处理更新操作。
            dealUpdate(parameter, loginId);
        }
    }

    /**
     * 提取的私有方法，该方法接收两个参数：
     *      一个Object类型的object和一个String类型的loginId。
     *
     *  用来处理insert语句的自动填充。
     */
    private void dealInsert(Object object, String loginId) {
        // 调用getAllFields方法获取object对象的所有字段。
        Field[] fields = getAllFields(object);
        // 遍历获取到的所有字段。
        for (Field field : fields) {
            try {
                // 打破private封装，将字段设置为可访问，这样我们可以修改它的值。
                field.setAccessible(true);
                // 获取字段在当前对象中的值。
                Object o = field.get(object);
                // 如果获取到的值不为null，表示该字段已有值，所以我们跳过此次循环。
                if (Objects.nonNull(o)) {
                    field.setAccessible(false); // 关闭字段的可访问性。
                    continue; // 跳过此次循环。
                }
                // 根据字段名称进行相应的更新操作。
                if ("createdBy".equals(field.getName())) {
                    // 如果字段名为"createBy"，则将其值设置为loginId。
                    field.set(object, loginId);
                    field.setAccessible(false); // 关闭字段的可访问性。
                } else if ("createdTime".equals(field.getName())) {
                    // 如果字段名为"createTime"，则将其值设置为当前日期。
                    field.set(object, new Date());
                    field.setAccessible(false); // 关闭字段的可访问性。
                } else {
                    // 对于其他字段，我们不进行任何操作，直接关闭字段的可访问性。
                    field.setAccessible(false);
                }
            } catch (Exception e) {
                // 如果在上述过程中发生异常，我们记录错误日志。
                log.error("dealUpdate.error:{}", e.getMessage(), e);
            }
        }
    }

    /**
     * 定义一个名为dealUpdate的方法，该方法接收两个参数：
     *      一个Object类型的object和一个String类型的loginId。
     *      
     *  用来处理update语句的自动填充。
     */
    private void dealUpdate(Object object, String loginId) {
        // 调用getAllFields方法获取object对象的所有字段。
        Field[] fields = getAllFields(object);
        // 遍历获取到的所有字段。
        for (Field field : fields) {
            try {
                // 打破private封装，将字段设置为可访问，这样我们可以修改它的值。
                field.setAccessible(true);
                // 获取字段在当前对象中的值。
                Object o = field.get(object);
                // 如果获取到的值不为null，表示该字段已有值，所以我们跳过此次循环。
                if (Objects.nonNull(o)) {
                    field.setAccessible(false); // 关闭字段的可访问性。
                    continue; // 跳过此次循环。
                }
                // 根据字段名称进行相应的更新操作。
                if ("updateBy".equals(field.getName())) {
                    // 如果字段名为"updateBy"，则将其值设置为loginId。
                    field.set(object, loginId);
                    field.setAccessible(false); // 关闭字段的可访问性。
                } else if ("updateTime".equals(field.getName())) {
                    // 如果字段名为"updateTime"，则将其值设置为当前日期。
                    field.set(object, new Date());
                    field.setAccessible(false); // 关闭字段的可访问性。
                } else {
                    // 对于其他字段，我们不进行任何操作，直接关闭字段的可访问性。
                    field.setAccessible(false);
                }
            } catch (Exception e) {
                // 如果在上述过程中发生异常，我们记录错误日志。
                log.error("dealUpdate.error:{}", e.getMessage(), e);
            }
        }
    }
    
    
    /**
     * 抽取一个私有方法。
     这段代码的核心思想是使用Java的反射API来获取给定对象的所有字段（包括继承的字段）。
     它通过获取对象的Class对象，然后获取该Class声明的所有字段，并将其添加到一个列表中。
     然后，它获取该Class的超类并重复这个过程，直到到达Object类。
     最后，它将找到的所有字段存储在一个Field数组中并返回。

     * 定义一个名为getAllFields的方法，该方法接收一个Object类型的参数，并返回一个Field类型的数组。
     */
    private Field[] getAllFields(Object object) {
        // 获取传入对象的Class对象。在Java中，每个对象都有一个与之关联的Class对象，该对象包含有关该对象类型的信息。
        Class<?> clazz = object.getClass();
        // 创建一个新的ArrayList来存储找到的Field对象。
        List<Field> fieldList = new ArrayList<>();

        // 开始一个循环，直到clazz为null（意味着我们已经到达了Object类，这是所有类的超类）为止。
        while (clazz != null) {
            // 将clazz类声明的所有字段添加到fieldList中。
            // 这里使用了Arrays.asList将Field数组转换为List，然后使用addAll方法将所有字段添加到fieldList中。
            fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields())));
            // 获取clazz的超类，并将其设置为新的clazz。这将继续循环，直到我们到达Object类。
            clazz = clazz.getSuperclass();
        }
        // 根据fieldList的大小创建一个新的Field数组。
        Field[] fields = new Field[fieldList.size()];
        // 将fieldList中的所有元素转换为Field数组并存储在fields数组中。
        fieldList.toArray(fields);
        // 返回包含所有字段的fields数组。
        return fields;
    }

    /**
     * 重写plugin方法，用于生成代理对象，使得在目标方法执行前后可以插入自定义的逻辑。
     */
    @Override
    public Object plugin(Object target) {
        // Plugin.wrap方法用于生成代理对象。
        //      其中target是要拦截的目标对象，this是指代当前Interceptor对象的引用。
        // 返回生成的代理对象。
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }
}
